package org.clockworkmages.games.anno1186.scripting;

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.clockworkmages.games.anno1186.GameCharacterService;
import org.clockworkmages.games.anno1186.GameDataService;
import org.clockworkmages.games.anno1186.GameGuiService;
import org.clockworkmages.games.anno1186.GameStateService;
import org.clockworkmages.games.anno1186.TimeConstants;
import org.clockworkmages.games.anno1186.gui.GameUiUtil;
import org.clockworkmages.games.anno1186.model.character.GameCharacter;
import org.clockworkmages.games.anno1186.model.character.GenderConstants;
import org.clockworkmages.games.anno1186.model.character.Mutation;
import org.clockworkmages.games.anno1186.model.character.Skill;
import org.clockworkmages.games.anno1186.model.character.Stat;
import org.clockworkmages.games.anno1186.model.common.Pair;
import org.clockworkmages.games.anno1186.model.effect.Condition;
import org.clockworkmages.games.anno1186.model.effect.ConditionStatus;
import org.clockworkmages.games.anno1186.model.effect.Perk;
import org.clockworkmages.games.anno1186.model.item.Item;
import org.clockworkmages.games.anno1186.model.situation.Situation;
import org.clockworkmages.games.anno1186.situations.combat.CombatSituation;
import org.clockworkmages.games.anno1186.situations.inventory.InventoryItemSituation;
import org.clockworkmages.games.anno1186.util.MathUtil;
import org.clockworkmages.games.anno1186.util.RollUtil;

public abstract class GameCharacterScriptingWrapper {

	protected abstract GameCharacter getGameCharacter();

	// condition methods

	/**
	 * Synonym to getEffectiveStat.
	 * 
	 * @param statName
	 * @return
	 */
	public double getStat(String statName) {
		return getEffectiveStat(statName);
	}

	public double getBaseStat(String statName) {
		return getStat(statName, getGameCharacter().getBaseStats());
	}

	public double getEffectiveStat(String statName) {
		return getStat(statName, getGameCharacter().getEffectiveStats());
	}

	public double getDerivate(String statName) {
		return getStat(statName, getGameCharacter().getDerivates());
	}

	private double getStat(String statName, Map<String, Double> map) {
		Double statValue = map.get(statName);
		return statValue == null ? 0 : statValue;
	}

	public int getSkillLevel(String skillId) {
		GameCharacter gameCharacter = getGameCharacter();
		Integer skillLevel = gameCharacter.getSkills().get(skillId);
		return skillLevel == null ? 0 : skillLevel.intValue();
	}

	public int getGold() {
		GameCharacter gameCharacter = getGameCharacter();
		return gameCharacter.getGold();
	}

	public boolean isMale() {
		int gender = getGameCharacter().getGender();
		return gender == GenderConstants.MALE;
	}

	public int getGender() {
		return getGameCharacter().getGender();
	}

	public void setGender(int gender) {
		getGameCharacter().setGender(gender);
	}

	public boolean isUnarmed() {
		return !getGameCharacter().getEquippedItems().containsKey("RIGHT_HAND");
	}

	// effect methods:

	public void addGold(int amount) {
		int newAmount = getGameCharacter().getGold() + amount;
		if (newAmount < 0) {
			newAmount = 0;
		}
		int difference = newAmount - getGameCharacter().getGold();
		if (difference != 0) {
			getGameCharacter().setGold(newAmount);
			if (isMe()) {
				getGameGuiService().getTextPane().addInfoToBuffer(
						"Your " + (difference > 0 ? "gained" : "lost") + " "
								+ Math.abs(difference) + " gold!\n");
			}
		}
	}

	public void setStat(String statName, double value) {
		getGameCharacter().getBaseStats().put(statName, value);
	}

	public void addPerk(String perkId) {
		GameCharacter gameCharacter = getGameCharacter();
		Perk perk = getGameDataService().getGameData().getPerks().get(perkId);
		if (perk == null) {
			GameUiUtil.logError("Invalid Perk reference: " + perkId);
		} else {
			gameCharacter.getPerks().add(perkId);
			scheduleRefresh();
		}
	}

	public void addRandomMutation() {
		GameCharacter gameCharacter = getGameCharacter();
		// TODO
		Mutation mutation = getGameDataService().getGameData().getMutations()
				.get("GOAT_HOOVES");
		getGameGuiService().getTextPane().addInfoToBuffer(
				(isMe() ? "You now have " : gameCharacter.getName()
						+ " now has ")
						+ mutation.getName() + "!\n");

		scheduleRefresh();
	}

	public void increaseStat(String statId, double increaseBy) {
		if (increaseBy == 0) {
			return;
		}
		GameCharacter gameCharacter = getGameCharacter();
		int roundedStatBefore = gameCharacter.getBaseStats()
				.containsKey(statId) ? (int) Math.floor(gameCharacter
				.getBaseStats().get(statId)) : 0;
		modifyStat(getGameCharacter().getBaseStats(), statId, increaseBy);
		int roundedStatAfter = (int) Math.floor(gameCharacter.getBaseStats()
				.get(statId));
		if (roundedStatBefore != roundedStatAfter) {
			int increase = roundedStatAfter - roundedStatBefore;
			Stat stat = getGameDataService().getGameData().getStats()
					.get(statId);
			if (stat != null) {
				String text = "";
				if (stat.isSocialStatus()) {
					text += stat.getName() + " ";
					if (roundedStatAfter >= 0) {
						text += "like "
								+ (isMe() ? "you" : gameCharacter.getName())
								+ " ";
						text += " a bit ";
						if (increaseBy > 0) {
							text += "more ";
						} else {
							text += "less ";
						}
					} else {
						if (roundedStatAfter < 25) {
							text += "hate "
									+ (isMe() ? "you" : gameCharacter.getName());
						} else {
							text += "dislike "
									+ (isMe() ? "you" : gameCharacter.getName());
						}
						text += " a bit ";
						if (increaseBy > 0) {
							text += "less ";
						} else {
							text += "more ";
						}
					}
					text += "now!";
				} else {
					text += (isMe() ? "Your " : gameCharacter.getName() + "'s ")
							+ stat.getName()
							+ " "
							+ (increase > 0 ? "increased" : "decreased")
							+ " by " + Math.abs(increase) + "!";
				}
				getGameGuiService().getTextPane().addInfoToBuffer(text + "\n");

			}
			scheduleRefresh();

		}

	}

	public void increaseSkill(String skillId, int increaseBy) {
		if (increaseBy == 0) {
			return;
		}
		GameCharacter gameCharacter = getGameCharacter();
		Integer currentValue = gameCharacter.getSkills().get(skillId);
		int newValue;
		if (currentValue == null) {
			newValue = increaseBy;
		} else {
			newValue = currentValue + increaseBy;
		}
		if (newValue < 1) {
			gameCharacter.getSkills().remove(skillId);
		} else {
			gameCharacter.getSkills().put(skillId, newValue);
		}

		Skill skill = getGameDataService().getGameData().getSkills()
				.get(skillId);
		getGameGuiService().getTextPane().addInfoToBuffer(
				(isMe() ? "Your " : gameCharacter.getName() + "'s ")
						+ skill.getName() + " skill "
						+ (increaseBy > 0 ? "increased" : "decreased") + " by "
						+ Math.abs(increaseBy) + "!\n");
		scheduleRefresh();
	}

	public void modifyDamage(String damageTypeId, double minDamage, double range) {
		Double oldValueMinimal = getGameCharacter().getDamageMinimals().get(
				damageTypeId);
		if (oldValueMinimal == null) {
			oldValueMinimal = 0d;
		}
		double newValueMin = oldValueMinimal + minDamage;
		getGameCharacter().getDamageMinimals().put(damageTypeId, newValueMin);

		Double oldValueRange = getGameCharacter().getDamageRanges().get(
				damageTypeId);
		if (oldValueRange == null) {
			oldValueRange = 0d;
		}
		double newValueRange = oldValueRange + range;
		getGameCharacter().getDamageRanges().put(damageTypeId, newValueRange);
	}

	public void modifyStat(String statName, double value) {
		modifyStat(getGameCharacter().getEffectiveStats(), statName, value);
	}

	public void modifyDerivate(String statName, double value) {
		modifyStat(getGameCharacter().getDerivates(), statName, value);
	}

	private void modifyStat(Map<String, Double> map, String statName,
			double value) {
		Double oldValue = map.get(statName);
		if (oldValue == null) {
			oldValue = 0d;
		}
		double newValue = oldValue + value;
		map.put(statName, newValue);
		scheduleRefresh();
	}

	public void modifyStatPerc(String statName, double percent) {
		modifyStatPerc(getGameCharacter().getEffectiveStats(), statName,
				percent);
	}

	public void modifyDerivatePerc(String statName, double percent) {
		modifyStatPerc(getGameCharacter().getDerivates(), statName, percent);
	}

	private void modifyStatPerc(Map<String, Double> map, String statName,
			double percent) {
		Double oldValue = map.get(statName);
		if (oldValue == null) {
			oldValue = 0d;
		}
		double newValue = oldValue * (100d + percent) / 100d;
		map.put(statName, newValue);
		scheduleRefresh();
	}

	public void gainExperience(int experience) {
		if (experience != 0) {
			GameCharacter gameCharacter = getGameCharacter();
			gameCharacter.setExperience(gameCharacter.getExperience()
					+ experience);
			if (isMe()) {
				getGameGuiService()
						.addInfo(
								"You have gained " + experience
										+ " experience points!");
				int level = gameCharacter.getLevel();
				int experienceNeededToLevelUp = MathUtil
						.experienceNeededToLevelUp(level + 1);
				if (experienceNeededToLevelUp <= gameCharacter.getExperience()) {
					getGameGuiService().addInfo(
							"You can now advance to a new level!");
				}
			}
		}
	}

	public void restoreAll() {
		getGameCharacterService().restoreAll(getGameCharacter());
	}

	public boolean skillRoll(String skillId, int difficulty) {
		int skillChance = skillChance(skillId, difficulty);
		double roll = getUtilScriptingWrapper().random(100);
		if (roll < skillChance) {
			return true;
		} else {
			return false;
		}
	}

	public boolean skillRoll(String skillId, int difficulty, String cacheKey) {
		int skillChance = skillChance(skillId, difficulty);
		double roll = getUtilScriptingWrapper().situationRandom(100, cacheKey);
		if (roll < skillChance) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Calculate the chance of success of a skill test.
	 * 
	 * The chance is equal to (1-0.98^(skillLevel*10+[average value of skill's
	 * ruling stats]*[skill's statModifier]))*100% .
	 * 
	 * @param skillId
	 * @param difficulty
	 *            difficulty of the test - 0 means average, positive means more
	 *            difficult, negative means easier.
	 * 
	 * @return
	 */
	public int skillChance(String skillId, int difficulty) {

		GameCharacter gameCharacter = getGameCharacter();

		Skill skill = getGameDataService().getGameData().getSkills()
				.get(skillId);
		Integer skillLevel = gameCharacter.getSkills().get(skillId);
		int pow = -difficulty;
		pow += MathUtil.zeroIfNull(skillLevel) * 10;

		for (String rulingStat : skill.getRulingStats()) {
			pow += MathUtil.zeroIfNull(gameCharacter.getEffectiveStats().get(
					rulingStat))
					/ skill.getStatModifier() / skill.getRulingStats().size();
		}

		int chance = getUtilScriptingWrapper().chance(pow);
		return chance;
	}

	public boolean statRoll(String statId, int difficulty) {
		int rollChance = statChance(statId, difficulty);
		double roll = getUtilScriptingWrapper().random(100);
		if (roll < rollChance) {
			return true;
		} else {
			return false;
		}
	}

	public boolean statRoll(String statId, int difficulty, String cacheKey) {
		int rollChance = statChance(statId, difficulty);
		double roll = getUtilScriptingWrapper().situationRandom(100, cacheKey);
		if (roll < rollChance) {
			return true;
		} else {
			return false;
		}
	}

	public int statChance(String statId, int difficulty) {

		GameCharacter gameCharacter = getGameCharacter();

		int statValue = gameCharacter.getEffectiveStatAsInt(statId);
		int pow = -difficulty;
		pow += MathUtil.zeroIfNull(statValue);

		int chance = RollUtil.chanceBase50(pow);
		return chance;
	}

	public void addItem(String itemId) {
		Item item = getGameDataService().getGameData().getItems().get(itemId);
		if (item == null) {
			GameUiUtil.logError("Invalid item reference: " + itemId);
			return;
		}
		GameCharacter character = getGameCharacter();
		character.getInventory().add(item);
		getGameGuiService().getTextPane().addInfoToBuffer(
				item.getName() + " has been added to your inventory!\n");
	}

	public boolean removeItem(String itemId) {
		GameCharacter character = getGameCharacter();
		Iterator<Item> i = character.getInventory().iterator();
		while (i.hasNext()) {
			Item item = i.next();
			if (item.getId().equals(itemId)) {
				i.remove();
				getGameGuiService().getTextPane().addInfoToBuffer(
						item.getName()
								+ " has been removed from your inventory.\n");
				return true;
			}
		}
		return false;
	}

	public void equipItemNoInv(String itemId) {
		Item item = getGameDataService().getGameData().getItems().get(itemId);
		if (item == null) {
			GameUiUtil.logError("Invalid item reference: " + itemId);
			return;
		}
		GameCharacter character = getGameCharacter();
		getGameGuiService().getTextPane().addInfoToBuffer(
				item.getName() + " equipped!\n");
		Item alreadyEquippedItem = character.getEquippedItems().get(
				item.getEquipmentSlotId());
		if (alreadyEquippedItem != null) {
			// move the item to Inventory
			character.getInventory().add(alreadyEquippedItem);
			getGameGuiService().getTextPane().addInfoToBuffer(
					alreadyEquippedItem.getName()
							+ " has been put back into your inventory!\n");
		}
		getGameCharacter().getEquippedItems().put(item.getEquipmentSlotId(),
				item);
	}

	public void equipItem(String itemId) {
		boolean isInInventory = removeItem(itemId);
		if (!isInInventory) {
			GameUiUtil
					.logError("Item could not be equipped because it is not in inventory:"
							+ itemId);
			return;
		} else {
			equipItemNoInv(itemId);
		}
	}

	public void unequipItem(String itemId) {
		GameCharacter character = getGameCharacter();
		for (Entry<String, Item> entry : character.getEquippedItems()
				.entrySet()) {
			Item item = entry.getValue();
			if (item.getId().equals(itemId)) {
				character.getEquippedItems().remove(entry.getKey());
				character.getInventory().add(item);
				getGameGuiService()
						.getTextPane()
						.addInfoToBuffer(
								item.getName()
										+ " has been unequipped and put back into your inventory.\n");
				break;
			}
		}
	}

	public void removeCurrentItem() {
		Situation situation = getGameStateService().getGameState()
				.getPreviousSituation();
		Item item = ((InventoryItemSituation) situation).getItem();
		removeItem(item.getId());
	}

	public void addCondition(String conditionId, int level, long milliseconds) {
		long validUntil = getGameStateService().getGameState().getTime()
				+ milliseconds;
		Condition condition = getGameDataService().getGameData()
				.getConditions().get(conditionId);
		if (condition == null) {
			GameUiUtil.logError("Invalid Condition reference: " + conditionId);
			return;
		} else {
			ConditionStatus conditionStatus = getGameCharacter()
					.getConditions().get(conditionId);
			if (conditionStatus == null) {
				conditionStatus = condition.makeStatus(level, validUntil);
				getGameCharacter().getConditions().put(conditionId,
						conditionStatus);
			} else {
				if (level > conditionStatus.getLevel()) {
					conditionStatus.setLevel(level);
				}
				if (milliseconds == TimeConstants.DURATION_UNLIMITED) {
					conditionStatus.setValidUntil(validUntil);
				} else if (conditionStatus.getValidUntil() == TimeConstants.DURATION_UNLIMITED) {
					// do nothing
				} else if (validUntil > conditionStatus.getValidUntil()) {
					conditionStatus.setValidUntil(validUntil);
				}
			}
			getGameGuiService().addInfo(condition.getDescription());
		}
	}

	public void incapacitate(String incapacitatedStateId, int difficulty) {
		if (!this.isMe()) {
			GameUiUtil
					.logError("Not (yet?) implemented! Cannot inflict an incapacitated state on an enemy!");
			return;
		}
		CombatSituation combatSituation = this.getGameStateService()
				.getCurrentCombatSituation();
		Pair<String, Integer> incapacitatedStateIdAndDificulty = new Pair<String, Integer>(
				incapacitatedStateId, difficulty);
		combatSituation.setIncapacitatedState(incapacitatedStateIdAndDificulty);
	}

	/**
	 * Calculate the character's chances of surviving a lost combat. They're
	 * equal to 100% if the character submitted (health>0%), 50% if the
	 * character lost at 0% health and then diminish, reaching 0% if the
	 * character finished combat at -25% health or less.
	 * 
	 * @return
	 */
	public boolean survived() {
		GameCharacter character = getGameCharacter();
		double healthPercent = character.getHealth() / character.getMaxHealth();
		if (healthPercent > 0) {
			// apparently the player surrendered
			return true;
		} else if (healthPercent > -0.25) {
			double random = getUtilScriptingWrapper().situationRandom(25,
					"SURVIVING_DEFEAT");
			if (healthPercent * (-100) < random) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}
	}

	public void healthAtLeast1() {
		GameCharacter character = getGameCharacter();
		if (character.getHealth() < 1) {
			character.setHealth(1d);
			scheduleRefresh();
		}
	}

	protected void scheduleRefresh() {
		getGameCharacter().setStatsAreStale(true);
	}

	protected abstract GameDataService getGameDataService();

	protected abstract GameStateService getGameStateService();

	protected abstract GameGuiService getGameGuiService();

	protected abstract GameCharacterService getGameCharacterService();

	protected abstract UtilScriptingWrapper getUtilScriptingWrapper();

	protected abstract boolean isMe();

}
